package com.tools.web.interceptor.auth;

import com.tools.common.exception.DataGenerationException;
import com.tools.common.object.Note;
import com.tools.common.security.secret.Cypher;
import com.tools.common.thread.Timeunit;
import com.tools.web.WebKit;
import com.tools.web.interceptor.access.AccessManager;
import com.tools.web.interceptor.access.AccessSource;
import com.tools.web.interceptor.access.own.DefaultAccessManager;
import com.tools.web.interceptor.access.spel.AuthorizeManager;
import org.slf4j.Logger;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.UUID;

/**
 * 用户认证管理器的公共逻辑抽象实现
 * */
@Note("用户认证管理器的公共逻辑抽象实现")
public abstract class GenericAuthenticationManager implements AuthenticationManager {

    @Note("token 是否在请求头中。false 的话就从请求参数中获取")
    private final boolean tokenFromHeader;

    @Note("token 的名称")
    private final String tokenName;

    @Note("【AuthToken 的规范】: 组成 token 各个部分的分隔符")
    private final String tokenSeparator;

    @Note("加密解密的 Cipher 实例和操作的包装类")
    private final Cypher cypher;

    @Note("缓存 AuthUser key 的前缀")
    private final String cacheKeyPrefix;

    @Note("【异常 JSON 信息】: 没有获取到 token")
    private final String tokenNotFoundErrorJson;

    @Note("【异常 JSON 信息】: token 不符合 AuthToken 的规范解析失败")
    private final String tokenParseFailedErrorJson;

    @Note("【异常 JSON 信息】: token 已过期")
    private final String tokenExpiredJson;

    @Note("是否开启 token 续期。默认关闭")
    private boolean enableExtend;

    @Note("token 每次续期续多长时间。【为 0 和负数不开启续期】。" +
            "该时间值的单位对应 protected_getTimeunitToBeUsed() 获取的单位")
    private long extendTime;

    @Note("从登录后的时间开始到这个时间点之内过期的 token 都允许续期。" +
            "【为 0、负数都不开启】。" +
            "若和 maxAllowCount 一起使用的话，该时间的优先级最高。")
    private long maxAllowExtendTime;

    @Note("从登录后的时间开始，可以允许 token 续期多少次。" +
            "【为 0、负数都不开启】。" +
            "若和 maxAllowExtendTime 一起使用的话，" +
            "则是：【在 maxAllowExtendTime 的时间内允许续期 maxAllowCount 次】")
    private int maxAllowCount;

    @Note("若过期时间临界点 - 现在 <= 该值，那就执行续期，无需等到真正过期。（必须大于 0 才生效）")
    private long minExtendThresholdTime;

    @Note("AuthUser 用户服务接口，用于认证过期后加载用户续期、获取角色/权限/路由等数据操作")
    private final AuthUserService authUserService;

    @Note("若想集成")
    private final AccessManager accessManager;


    /* **************************************************************************************
     *
     *          构造方法
     *
     * **************************************************************************************
     * */

    protected GenericAuthenticationManager(AuthSource source, AuthUserService authUserService) {
        this(source, authUserService, null);
    }

    protected GenericAuthenticationManager(AuthSource source, AuthUserService authUserService,
                                           AccessManager accessManager) {
        if(source == null) throw new NullPointerException("AuthSource 不能为 null");
        if(authUserService == null) throw new NullPointerException("AuthUserService 不能为 null");
        this.tokenFromHeader = source.isTokenFromHeader();
        this.tokenName = source.getTokenName();
        this.tokenSeparator = source.getTokenSeparator();
        this.cypher = source.getCypher();
        String cacheKeyPrefix = source.getCacheKeyPrefix();
        this.cacheKeyPrefix = cacheKeyPrefix == null || cacheKeyPrefix.isEmpty()
                ? "AT:"
                : cacheKeyPrefix;
        this.tokenNotFoundErrorJson = source.getTokenNotFoundErrorJson();
        this.tokenParseFailedErrorJson = source.getTokenParseFailedErrorJson();
        this.tokenExpiredJson = source.getTokenExpiredJson();
        this.authUserService = authUserService;
        this.accessManager = this.private_buildAccessManager(source);
        TokenExtender tokenExtender = source.getTokenExtender();
        if(tokenExtender == null) return;
        this.enableExtend = tokenExtender.isAllowedExtend()
                && (tokenExtender.isUseMaxAllowExtendTime() || tokenExtender.isUseMaxAllowCount());
        Timeunit toBeUsedTimeunit = this.protected_getTimeunitToBeUsed();
        this.extendTime = toBeUsedTimeunit.cast(tokenExtender.getExtendTime());
        if(tokenExtender.isUseMaxAllowExtendTime()) {
            this.maxAllowExtendTime = toBeUsedTimeunit.cast(tokenExtender.getMaxAllowExtendTime());
        }
        if(tokenExtender.isUseMaxAllowCount()) {
            this.maxAllowCount = tokenExtender.getMaxAllowCount();
        }
        this.minExtendThresholdTime = toBeUsedTimeunit.cast(tokenExtender.getMinExtendThresholdTime());
    }




    /* **************************************************************************************
     *
     *          核心拦截逻辑
     *
     * **************************************************************************************
     * */

    @Override
    public final boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取被加密的 token
        String encryptedToken = this.tokenFromHeader
                ? request.getHeader(this.tokenName)
                : request.getParameter(this.tokenName);
        if(encryptedToken == null || encryptedToken.isEmpty()) {
            response.getWriter().write(this.tokenNotFoundErrorJson);
            return false;
        }
        //解密
        AuthToken decryptedToken;
        try {
            decryptedToken = new AuthToken(encryptedToken, this.cypher, this.tokenSeparator);
        } catch (Exception e) {
            this.protected_getLogger().info("token: {} 解析失败, 原因是: {}", encryptedToken, e.getMessage());
            response.getWriter().write(this.tokenParseFailedErrorJson);
            return false;
        }
        //获取用户认证对象
        String key = this.cacheKeyPrefix + decryptedToken.getUserId()
                + decryptedToken.userTypeAsPrefix();
        Res res = this.getAuthUser(key, decryptedToken);
        //获取的用户认证对象为 null 则判断是否可以续期，可以则续，否则返回错误信息
        if(res.authUserIsNull()) {
            if(res.isCanExtend()) {
                return this.private_extendTheLifeOfTokensOrBlockForward(request, response, decryptedToken, handler);
            }
            response.getWriter().write(this.tokenExpiredJson);
            return false;
        }
        AuthUser authUser = res.getAuthUser();
        //过期时间临界点 - 现在 <= 该值，那就执行续期。无需等到真正过期
        long now = System.currentTimeMillis();
        long expiration = decryptedToken.getExpiration();
        if(this.minExtendThresholdTime > 0 && (expiration - now) <= this.minExtendThresholdTime) {
            return this.private_extendTheLifeOfTokensOrBlockForward(request, response, decryptedToken, handler);
        }
        return this.private_allowGo(request, response, encryptedToken, authUser, handler);
    }

    /* **************************************************************************************
     *
     *          子类必须实现的抽象方法
     *
     * **************************************************************************************
     * */

    @Note("从缓存中获取 MultiClientAuthUser 对象")
    protected abstract Res getAuthUser(String key, AuthToken token);

    @Note("将 MultiClientAuthUser 对象放入缓存中，并设置过期时间")
    protected abstract void setAuthUser(String key, AuthUser srcAuthUser, AuthToken token);

    @Note("将指定的用户过期")
    protected abstract void removeAuthUser(String key);

    @Note("更新用户数据对象")
    protected abstract void updateAuthUser(String key, AuthUser authUser, String separator);

    @Note("获取子类实现的日志对象")
    protected abstract Logger protected_getLogger();

    @Note("获取子类所要使用的时间单位。如 Redis 实现那就是秒，本地缓存实现那就是毫秒")
    protected abstract Timeunit protected_getTimeunitToBeUsed();

    @Note("子类实现的具体登出逻辑")
    protected abstract void protected_loginOut(String key, String deviceTypeId,
                                               String uuid, AuthUser authUser,
                                               String separator);

    /* **************************************************************************************
     *
     *           提供给子类使用的方法
     *
     * **************************************************************************************
     * */


    @Note("判断 token 是否已过期，是则返回允许续期的结果，否则不允许续期")
    protected final Res protected_getResIfExpired(AuthToken token) {
        return token.ifExpired(this.protected_getTimeunitToBeUsed())
                ? new Res(true, null)
                : new Res(false, null);
    }



    @Note("检查配置的设备类型 ID 和允许最大在线数量是否正确")
    protected final void protected_checkDeviceTypeIdAndMaxCountMap(Map<String, Integer> deviceTypeIdAndMaxCountMap) {
        for (Map.Entry<String, Integer> e : deviceTypeIdAndMaxCountMap.entrySet()) {
            String deviceTypeId = e.getKey();
            Integer maxCount = e.getValue();
            if(!LoginDeviceTypeEnum.contains(deviceTypeId)) {
                throw new IllegalArgumentException("不知道 " + deviceTypeId + " 是什么设备类型");
            }
            if(maxCount == null) {
                throw new IllegalArgumentException(deviceTypeId + " 的最大在线设备数量为 null");
            }
        }
    }

    /* **************************************************************************************
     *
     *           实现接口的方法
     *
     * **************************************************************************************
     * */

    @Override
    public final AuthUserService getAuthUserService() {
        return authUserService;
    }

    @Override
    @Note("登录后续的认证流程")
    public final void handleLogin(Object userId, String userType, Object user, long expiration,
                            Timeunit timeunit, HttpServletResponse response, String deviceTypeId) {
        Timeunit toBeUsedTimeunit = this.protected_getTimeunitToBeUsed();
        long realExpiration = toBeUsedTimeunit.cast(expiration, timeunit);
        AuthToken token = new AuthToken(this.cypher, this.tokenSeparator);
        token.setUserId(userId.toString());
        token.setDeviceTypeId(deviceTypeId);
        token.setUserType(userType);
        token.setExpiration(realExpiration);
        token.setUuid(UUID.randomUUID().toString());
        token.setExtendedCount(0);
        token.setLoginTime(System.currentTimeMillis());
        //加密 token 并设置到响应头里，开放给客户端
        String encryptedToken = token.encrypt();
        response.setHeader(this.tokenName, encryptedToken);
        WebKit.openHeader(response, this.tokenName);
        //将用户对象和加载处理的授权数据一并组合成 AuthUser 对象，然后放入缓存中
        AuthUser authUser = this.authUserService.loadAuthUser(user, userId, userType);
        String key = this.cacheKeyPrefix + userId + token.userTypeAsPrefix();
        this.setAuthUser(key, authUser, token);
    }


    @Override
    @Note("封禁用户操作")
    public final void handleDisable(HttpServletRequest request) {
        String encryptedToken = this.tokenFromHeader
                ? request.getHeader(this.tokenName)
                : request.getParameter(this.tokenName);
        if(encryptedToken == null || encryptedToken.isEmpty()) return;
        AuthToken decryptedToken;
        try {
            decryptedToken = new AuthToken(encryptedToken, this.cypher, this.tokenSeparator);
        } catch (Exception e) {
            this.protected_getLogger().error("【封禁操作】：解密 token: {} 失败，暂无法执行封禁操作，失败信息: {}",
                    encryptedToken, e.getMessage());
            return;
        }
        String userId = decryptedToken.getUserId();
        String userTypePrefix = decryptedToken.userTypeAsPrefix();
        //移除缓存，防止再有请求过来
        this.removeAuthUser(this.cacheKeyPrefix + userId + userTypePrefix);
        //封禁操作
        int disabledResult = this.authUserService.disabledUser(userId, decryptedToken.getUserType());
        Logger logger = this.protected_getLogger();
        if(disabledResult > 0) {
            logger.info("封禁 ID 为 【{}】 的用户/账户成功!", userId);
        } else {
            logger.info("封禁 ID 为 【{}】 的用户/账户失败，请查看 SQL 日志以检查修改失败的原因!", userId);
        }
    }


    @Override
    @Note("退出登录的操作")
    public final void handleLoginOut(HttpServletRequest request) {
        AuthUser authUser = (AuthUser) request.getAttribute(USER_DATA_NAME);
        String userId = authUser.getUserId().toString();
        String userType = authUser.getUserType();
        String suffix = (userType != null && !userType.isEmpty()) ? (this.tokenSeparator + userType) : "";
        String key = this.cacheKeyPrefix + userId + suffix;
        String deviceTypeId = authUser.getDeviceTypeId();
        String uuid = authUser.getUuid();
        this.protected_loginOut(key, deviceTypeId, uuid, authUser, this.tokenSeparator);
    }

    @Override
    @Note("更新 “用户数据/角色/权限/路由/自定义数据” 等操作时的后续同步更新操作")
    public final void handleUpdate(Object userId, String userType) {
        AuthUser newAuthUser = this.authUserService.loadAuthUser(userId, userType);
        this.handleUpdate(newAuthUser);
    }

    @Override
    @Note("更新 “用户数据/角色/权限/路由/自定义数据” 等操作时的后续同步更新操作")
    public final void handleUpdate(AuthUser newAuthUser) {
        String userType = newAuthUser.getUserType();
        Object userId = newAuthUser.getUserId();
        String suffix = (userType != null && !userType.isEmpty()) ? (this.tokenSeparator + userType) : "";
        String key = this.cacheKeyPrefix + userId + this.tokenSeparator + suffix;
        this.updateAuthUser(key, newAuthUser, this.tokenSeparator);
    }

    /* **************************************************************************************
     *
     *          私有逻辑
     *
     * **************************************************************************************
     * */


    @Note("设置必须的数据，并且将 token 放到响应头里")
    private boolean private_allowGo(HttpServletRequest request, HttpServletResponse response,
                                 String token, AuthUser authUser, Object handler) throws Exception {
        response.setHeader(this.tokenName, token);
        WebKit.openHeader(response, this.tokenName);
        request.setAttribute(USER_DATA_NAME, authUser);
        return accessManager == null || accessManager.preAuthorize(request, response, handler);
    }


    @Note("如果开启 【续期功能】 就为 token 续期然后放行，否则就拦截")
    private boolean private_extendTheLifeOfTokensOrBlockForward(HttpServletRequest request, HttpServletResponse response,
                                                             AuthToken decryptedToken, Object handler) throws Exception {
        //不开启续期，就直接响应过期的信息
        if(!this.enableExtend) {
            response.getWriter().write(this.tokenExpiredJson);
            return false;
        }
        //现在时间 - 登录时的时间 >= 最大允许的期限，则不允许续期
        if(maxAllowExtendTime > 0) {
            long loginTime = decryptedToken.getLoginTime();
            Timeunit toBeUsedTimeunit = this.protected_getTimeunitToBeUsed();
            long passedTime = toBeUsedTimeunit.cast(System.currentTimeMillis() - loginTime,
                    Timeunit.MILL);
            if(passedTime >= maxAllowExtendTime) {
                response.getWriter().write(this.tokenExpiredJson);
                return false;
            }
        }
        //已续期的次数 >= 最大允许的次数，则不允许续期
        if(maxAllowCount > 0) {
            int extendedCount = decryptedToken.getExtendedCount();
            if(extendedCount >= maxAllowCount) {
                response.getWriter().write(this.tokenExpiredJson);
                return false;
            }
            extendedCount = extendedCount + 1;
            decryptedToken.setExtendedCount(extendedCount);
        }
        //续期
        //如果重新加载的用户还是为 null，要么是用户 ID 出错，该 token 是非法的。要么就是用户已被删除。直接响应过期即可。
        AuthUser authUser = this.authUserService.loadAuthUser(decryptedToken.getUserId(),
                decryptedToken.getUserType());
        if(authUser == null) {
            response.getWriter().write(this.tokenExpiredJson);
            return false;
        }
        String newUUID = UUID.randomUUID().toString().replace("-", "");
        decryptedToken.setUuid(newUUID);
        long expiration = decryptedToken.getExpiration();
        Timeunit toBeUsedTimeunit = this.protected_getTimeunitToBeUsed();
        long now = toBeUsedTimeunit.cast(System.currentTimeMillis(), Timeunit.MILL);
        expiration = Math.max(expiration, now);
        expiration = expiration + this.extendTime;
        decryptedToken.setExpiration(expiration);
        String encryptedToken = decryptedToken.encrypt();
        String key = this.cacheKeyPrefix + decryptedToken.getUserId()
                + decryptedToken.userTypeAsPrefix();
        this.setAuthUser(key, authUser, decryptedToken);
        //设置必须的信息，并放行
        return this.private_allowGo(request, response, encryptedToken, authUser, handler);
    }


    @Note("初始化授权管理器类，开启授权功能")
    private AccessManager private_buildAccessManager(AuthSource source) {
        //在认证管理器里开启授权功能
        String accessManagerClassName = source.getAccessManagerClassName();
        if(accessManagerClassName == null || accessManagerClassName.isEmpty()) return null;
        AccessSource accessSource = source.getAccessSource();
        if(AuthorizeManager.class.getName().equals(accessManagerClassName)) {
            return new AuthorizeManager(accessSource);
        }
        if(DefaultAccessManager.class.getName().equals(accessManagerClassName)) {
            return  new DefaultAccessManager(accessSource);
        }
        throw new DataGenerationException("您已在认证管理器里开启了授权功能，但是暂不支持 " + accessManagerClassName
                + " 授权管理器类");
    }

    /* **************************************************************************************
     *
     *          内部类
     *
     * **************************************************************************************
     * */

    @Note("从缓存中获取回来的结果类")
    public static class Res {
        private final boolean canExtend;
        private final AuthUser authUser;

        protected Res(boolean canExtend, AuthUser authUser) {
            this.canExtend = canExtend;
            this.authUser = authUser;
        }

        private boolean isCanExtend() { return canExtend; }
        private AuthUser getAuthUser() { return authUser; }
        private boolean authUserIsNull() { return authUser == null; }
    }
}
